不同版本上 Bitmap 内存分配与回收对比
The following article is from 彭旭锐 Author 彭旭锐
前言
Bitmap 是 Android 应用的内存占用大户,是最容易造成 OOM 的场景。为此,Google 也在不断尝试优化 Bitmap 的内存分配和回收策略,涉及:Java 堆、Native 堆、硬件等多种分配方案,未来会不会有新的方案呢?
深入理解 Bitmap 的内存模型是有效开展图片内存优化的基础,在这篇文章里,我将深入 Android 6.0 和 Android 8.0 系统源码,为你总结出不同系统版本上的 Bitmap 运行时内存模型,以及 Bitmap 使用的 Native 内存回收兜底策略。知其然,知其所以然,开干!
学习路线图
1. 不同版本的 Bitmap 内存分配策略
先说一下 Bitmap 在内存中的组成部分,在任何系统版本中都会存在以下 3 个部分:
其中,Java Bitmap 对象和 Native Bitmap 对象是分别存储在 Java 堆和 Native 堆的,毋庸置疑。唯一有操作性的是 3、图片像素数据,不同系统版本采用了不同的分配策略,分为 3 个历史时期:
源码摘要如下:
Android 7.1 Bitmap.java
http://androidxref.com/7.1.1_r6/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java
// Native 层 Bitmap 指针
private final long mNativePtr;
// 像素数据
private byte[] mBuffer;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
Android 8.0 Bitmap.java
http://androidxref.com/8.0.0_r4/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java
// Native 层 Bitmap 指针
private final long mNativePtr;
// 这部分存在 Native 层
// private byte[] mBuffer;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
1.2 不同版本的 Bitmap 内存回收兜底策略
Java Bitmap 对象提供了 recycle() 方法主动释放内存资源。然而, 由于 Native 内存不属于 Java 虚拟机垃圾收集管理的区域,如果不手动调用 recycle() 方法释放资源,即使 Java Bitmap 对象被垃圾回收,位于 Native 层的 Native Bitmap 对象和图片像素数据也不会被回收的。为了避免 Native 层内存泄漏,Bitmap 内部增加了兜底策略,分为 2 个历史时期:
用一个表格总结:
分配策略 | 回收兜底策略 | |
---|---|---|
Android 7.0 以前 | Java 堆 | Finalizer 机制 |
Android 7.0 / Android 7.1 | Java 堆 | 引用机制 |
Android 8.0 以后 | Native 堆 / 硬件 | 引用机制 |
关于 Finalizer 机制和引用机制的深入分析,见 Finalizer 机制。
https://juejin.cn/post/7131027741163388958
程序验证:我们通过一段程序作为佐证,在 Android 8.0 模拟分配创建 Bitmap 对象后未手动调用 recycle() 方法,观察 Native 内存是否会回收。
示例程序
// 模拟创建 Bitmap 但未主动调用 recycle()
tv.setOnClickListener{
val map = HashSet<Any>()
for(index in 0 .. 2){
map.add(BitmapFactory.decodeResource(resources, R.drawable.test))
}
}
GC 前的内存分配情况:
GC 后的内存分配情况:
可以看到加载图片后 Native 内存有明显增大,而 GC 后 Native 内存同步下降,符合预期。
1.3 没有必要主动调用 recycle() 吗?
由于 Bitmap 使用了 Finalizer 机制或引用机制来辅助回收,所以当 Java Bitmap 对象被垃圾回收时,也会顺带回收 Native 内存。出于这个原因,网上有观点认为 Bitmap 已经没有必要主动调用 recycle() 方法了,甚至还说是 Google 建议的。真的是这样吗,我们看下 Google 原话是怎么说的:
不得不说,Google 这番话确实是有误导性, not need to be called 确实是不需要 / 不必要的意思。抛开这个字眼,我认为 Google 的意思是想说明有兜底策略的存在,如果开发者没有调用 recycle() 方法,也不必担心内存泄漏。如果开发者主动调用 recycle() 方法,则可以获得 advanced 更好的性能 。
再进一步抛开 Google 的观点,站在我们的视角独立思考,你认为需要主动调用 recycle() 方法吗?需要。Finalizer 机制和引用机制的定位是清晰明确的,它们都是 Bitmap 用来辅助回收内存的兜底策略。虽然从 Finalizer 机制升级到引用机制后稳定性略有提升,或者将来从引用机制升级到某个更优秀的机制,不管怎么升级,兜底策略永远是兜底策略,它永远不会也不能替换主要策略:在不需要使用资源时立即释放资源。举个例子,Glide 内部的 Bitmap 缓存池在清除缓存时,会主动调用 recycle() 吗?看源码:
LruBitmapPool.java
// 已简化
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
currentSize -= strategy.getSize(removed);
// 主动调用 recycle()
removed.recycle();
}
}
这一节,我们来分析 Bitmap 的创建过程。由于 Android 8.0 前后采用了不同的内存分配方案,而 Android 7.0 前后采用了不同的内存回收兜底方案,综合考虑我选择从 Android 6.0 和 Android 8.0 展开分析:
2.1 BitmapFactory 工厂类
Bitmap 的构造方法是非公开的,创建 Bitmap 只能通过 BitmapFactory 或 Bitmap 的静态方法创建,即使 ImageDecoder 内部也是通过 BitmapFactory 创建 Bitmap 的。
BitmapFactory 工厂类提供了从不同数据源加载图片的能力,例如资源图片、本地图片、内存中的 byte 数组等。不管怎么样,最终还是通过 native 方法来创建 Bitmap 对象,下面我们以 nativeDecodeStream(…) 为例展开分析。
// 解析资源图片
public static Bitmap decodeResource(Resources res, int id)
// 解析本地图片
public static Bitmap decodeFile(String pathName)
// 解析文件描述符
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
// 解析 byte 数组
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
// 解析输入流
public static Bitmap decodeStream(InputStream is)
// 最终通过 Native 层创建 Bitmap 对象
private static native Bitmap nativeDecodeStream(...);
private static native Bitmap nativeDecodeFileDescriptor(...);
private static native Bitmap nativeDecodeAsset(...);
private static native Bitmap nativeDecodeByteArray(...);
2.2 Android 8.0 创建过程分析
Android 8.0 之前的版本相对过时了,我决定把精力向更时新的版本倾斜,所以我们先分析 Android 8.0 中的创建过程。Java 层调用的 native 方法最终会走到 doDecode(…) 函数中,内部的逻辑非常复杂,我将整个过程概括为 5 个步骤:
源码摘要如下:
Android 8.0 BitmapFactory.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp#doDecode
// Java native 方法关联的 JNI 函数
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {
// 已简化
return doDecode(env, bufferedStream.release(), padding, options);
}
// 核心方法
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
// 省略 BitmapFactory.Options 参数读取
// 1. 创建解码器
NinePatchPeeker peeker;
std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(streamDeleter.release(), &peeker));
// 2. 创建内存分配器
// HeapAllocator:在 Native Heap 分配内存
HeapAllocator defaultAllocator;
SkBitmap::Allocator* decodeAllocator = &defaultAllocator;
SkBitmap decodingBitmap;
// 图片参数信息(在下文源码中会用到)
const SkImageInfo bitmapInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace);
// 3. 预分配像素数据内存
// tryAllocPixels():创建 Native Bitmap 对象并预分配像素数据内存
if (!decodingBitmap.setInfo(bitmapInfo) || !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
// 异常 1:Java OOM
// 异常 2:Native OOM
// 异常 3:复用已调用 recycle() 的 Bitmap
return nullptr;
}
// 4. 解码
// getAndroidPixel():解码并写入像素数据内存地址
// getPixels():像素数据内存地址
// rowBytes():像素数据大小
SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(), decodingBitmap.rowBytes(), &codecOptions);
switch (result) {
case SkCodec::kSuccess:
case SkCodec::kIncompleteInput:
break;
default:
return nullObjectReturn("codec->getAndroidPixels() failed.");
}
// 省略 .9 图逻辑
// 省略 sample 缩放逻辑
// 省略 inBitmap 复用逻辑
// 省略 Hardware 硬件位图逻辑
// 5. 创建 Java Bitmap 对象
// defaultAllocator.getStorageObjAndReset():获取 Native 层 Bitmap 对象
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
中间几个步骤的源码先放到一边,我们先把注意力放到决定函数返回值最后一个步骤上。
步骤 5 - 返回 Java Bitmap 对象 源码分析:
Android 8.0 graphics/Bitmap.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) {
...
// 5.1 创建 BitmapWrapper 包装类
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
// 5.2 调用 Java 层 Bitmap 构造函数
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);
return obj;
}
// BitmapWrapper 是对 Native Bitmap 的包装类,本质还是 Native Bitmap
class BitmapWrapper {
public:
BitmapWrapper(Bitmap* bitmap) : mBitmap(bitmap) { }
...
private:
// Native Bitmap 指针
sk_sp<Bitmap> mBitmap;
...
};
Java 层 Bitmap 构造函数:
Android 8.0 Bitmap.java
// Native Bitmap 指针
private final long mNativePtr;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
// 从 JNI 层调用
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
// 宽度
mWidth = width;
// 高度
mHeight = height;
// .9 图信息
mNinePatchChunk = ninePatchChunk;
// Native Bitmap 指针
mNativePtr = nativeBitmap;
...
}
可以看到,第 5 步是调用 Java Bitmap 的构造函数创建 Java Bitmap 对象,并传递一个 Native Bitmap 指针 nativeBitmap。至此,Bitmap 对象创建完毕,Java Bitmap 持有一个指向 Native Bitmap 的指针,像素数据由 Native 管理。
现在,我们回过头来分析下 doDecode(…) 中间的其它步骤:
步骤 3 - 预分配像素数据内存源码分析:
HeapAllocator 是默认的分配器,用于在 Native Heap 上分配像素数据内存。内部经过一系列跳转后,最终核心的源码分为 4 步:
源码摘要如下:
Android 8.0 SkBitmap.cpp
http://androidxref.com/8.0.0_r4/xref/external/skia/src/core/SkBitmap.cpp
// 3. 创建 Native Bitmap 对象并预分配像素数据内存
bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
return allocator->allocPixelRef(this, ctable);
}
HeapAllocator 内存分配器的定义在 GraphicsJNI.h / Graphics.cpp 中:
Android 8.0 GraphicsJNI.h
http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/GraphicsJNI.h
class HeapAllocator : public SkBRDAllocator {
public:
// 3.1 分配内存函数原型
virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) override;
// 返回 Native Bitmap 的指针
android::Bitmap* getStorageObjAndReset() {
return mStorage.release();
};
SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kYes_ZeroInitialized; }
private:
// Native Bitmap 的指针
sk_sp<android::Bitmap> mStorage;
};
Android 8.0 Graphics.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Graphics.cpp
// 3.2 分配内存函数实现
// 创建 Native Bitmap 对象,并将指针记录到 HeapAllocator#mStorage 字段中
bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
// 3.4 记录 Native Bitmap 的指针
mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
return !!mStorage;
}
真正开始分配内存的地方:
Android 8.0 hwui/Bitmap.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp
// AllocPixeRef 为函数指针,类似于 Kotlin 的高阶函数
typedef sk_sp<Bitmap> (*AllocPixeRef)(size_t allocSize, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable);
// 3.3 真正开始创建
sk_sp<Bitmap> Bitmap::allocateHeapBitmap(SkBitmap* bitmap, SkColorTable* ctable) {
// 第三个参数是指向 allocateHeapBitmap 的函数指针
return allocateBitmap(bitmap, ctable, &android::allocateHeapBitmap);
}
// 第三个参数为函数指针
static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, SkColorTable* ctable, AllocPixeRef alloc) {
// info:图片参数
// size:像素数据内存大小
// rowBytes:一行占用的内存大小
// 3.3.1 获取图片参数信息(SkImageInfo 在上文提到了)
const SkImageInfo& info = bitmap->info();
size_t size;
const size_t rowBytes = bitmap->rowBytes();
// 3.3.2 计算像素数据内存大小,并将结果赋值到 size 变量上
if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
return nullptr;
}
// 3.3.3 创建 Native Bitmap 对象并分配像素数据内存空间
auto wrapper = alloc(size, info, rowBytes, ctable);
// 3.3.4 关联 SkBitmap 与 Native Bitmap
wrapper->getSkBitmap(bitmap);
bitmap->lockPixels();
return wrapper;
}
// 函数指针指向的函数
// 3.3.2 创建 Native Bitmap 对象并预分配像素数据内存
static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable) {
// 3.3.2.1 使用库函数 calloc 分配 size*1 的连续空间
void* addr = calloc(size, 1);
// 3.3.2.2 创建 Native Bitmap 对象
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
}
// 3.3.2.2 Native Bitmap 构造函数
Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
: SkPixelRef(info)
, mPixelStorageType(PixelStorageType::Heap) {
// 指向像素数据的内存指针(在回收过程源码中会用到)
mPixelStorage.heap.address = address;
// 像素数据大小
mPixelStorage.heap.size = size;
reconfigure(info, rowBytes, ctable);
}
// 3.3.3 关联 SkBitmap 与 Native Bitmap
void Bitmap::getSkBitmap(SkBitmap* outBitmap) {
...
// 让 SkBitmap 持有 Native Bitmap 的指针,SkBitmap 会解析出像素数据的指针
outBitmap->setPixelRef(this);
}
至此,Native Bitmap 和像素数据内存空间都准备好了,SkBitmap 也成功获得了指向 Native 堆像素数据的指针。下一步就由 Skia 引擎的解码器对输入流解码并写入这块内存中,Skia 引擎我们下次再讨论,我们今天主要讲 Bitmap 的核心流程。
2.3 Android 6.0 创建过程分析
现在我们来分析 Android 6.0 上的 Bitmap 创建过程,理解 Android 8.0 的分配过程后就驾轻就熟了。Java 层调用的 native 方法最终也会走到 doDecode(…) 函数中,内部的逻辑非常复杂,我将整个过程概括为 5 个步骤:
好家伙,创建过程不能说类似,只能说完全一样。直接上源码摘要:
Android 6.0 BitmapFactory.cpp
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp#doDecode
// Java native 方法关联的 JNI 函数
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {
// 已简化
return doDecode(env, bufferedStream.release(), padding, options);
}
// 核心方法
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
// 省略 BitmapFactory.Options 参数读取
// 1. 创建解码器
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
NinePatchPeeker peeker(decoder);
decoder->setPeeker(&peeker);
// 2. 创建内存分配器
JavaPixelAllocator javaAllocator(env);
decoder->setAllocator(javaAllocator);
// 3. 预分配像素数据内存
// 4. 解码
// decode():创建 Native Bitmap 对象、预分配像素数据内存、解码
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode) != SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}
// 省略 .9 图逻辑
// 省略 sample 缩放逻辑
// 省略 inBitmap 复用逻辑
// 5. 创建 Java Bitmap 对象
// javaAllocator.getStorageObjAndReset():获取 Native 层 Bitmap 对象
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
中间几个步骤的源码先放到一边,我们同样先把注意力放到决定函数返回值最后一个步骤上。
步骤 5 - 返回 Java Bitmap 对象 源码分析:
Android 6.0 Graphics.cpp
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/Graphics.cpp
jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
int density) {
// 调用 Java 层 Bitmap 构造函数
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
ninePatchChunk, ninePatchInsets);
return obj;
}
Java 层 Bitmap 构造函数:
Android 6.0 Bitmap.java
http://androidxref.com/6.0.1_r10/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java
// Native Bitmap 指针
private final long mNativePtr;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
// 从 JNI 层调用
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
// 宽度
mWidth = width;
// 高度
mHeight = height;
// .9 图信息
mNinePatchChunk = ninePatchChunk;
// Native Bitmap 指针
mNativePtr = nativeBitmap;
}
可以看到,第 5 步是调用 Java Bitmap 的构造函数创建 Java Bitmap 对象,并传递一个 Native Bitmap 指针 nativeBitmap 和一个 byte[] 对象 buffer。至此,Bitmap 对象创建完毕,Java Bitmap 持有一个指向 Native Bitmap 的指针,像素数据由 Java 管理。
现在,我们回过头来分析下 doDecode(…) 中间的其它步骤:
步骤 3 - 预分配像素数据内存源码分析:
Android 6.0 这边将步骤 3 和步骤 4 都放在解码器 SkImageDecoder::decode 中,最终通过模板方法 onDecode() 让子类实现,我们以 PNG 的解码器为例。
Android 6.0 SkImageDecoder.cpp
http://androidxref.com/6.0.1_r10/xref/external/skia/src/images/SkImageDecoder.cpp
SkImageDecoder::Result SkImageDecoder::decode(SkStream* stream, SkBitmap* bm, SkColorType pref, Mode mode) {
SkBitmap tmp;
// onDecode 由子类实现
const Result result = this->onDecode(stream, &tmp, mode);
if (kFailure != result) {
bm->swap(tmp);
}
return result;
}
Android 6.0 SkImageDecoder_libpng.cpp
http://androidxref.com/6.0.1_r10/xref/external/skia/src/images/SkImageDecoder_libpng.cpp
SkImageDecoder::Result SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap, Mode mode) {
...
// 3. 预分配像素数据内存
if (!this->allocPixelRef(decodedBitmap, kIndex_8_SkColorType == colorType ? colorTable : NULL)) {
return kFailure;
}
// 4. 解码
...
}
相似的流程我们就不要过度分析了,反正也是通过 JavaPixelAllocator 分配内存的。JavaPixelAllocator 最终调用 allocateJavaPixelRef() 创建 Native Bitmap 对象:
Android 6.0 Graphics.cpp
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/Graphics.cpp
android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap, SkColorTable* ctable) {
// info:图片参数
// size:像素数据内存大小
// rowBytes:一行占用的内存大小
// 3.1 获取图片参数信息(SkImageInfo 在上文提到了)
const SkImageInfo& info = bitmap->info();
size_t size;
// 3.2 计算像素数据内存大小,并将结果赋值到 size 变量上
if (!computeAllocationSize(*bitmap, &size)) {
return NULL;
}
const size_t rowBytes = bitmap->rowBytes();
// 3.3 创建 Java byte 数组对象,数组大小为 size
jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime, gVMRuntime_newNonMovableArray, gByte_class, size);
// 3.4 获取 byte 数组
jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
// 3.5 创建 Native Bitmap 对象
android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable);
// 3.6 关联 SkBitmap 与 Native Bitmap
wrapper->getSkBitmap(bitmap);
bitmap->lockPixels();
return wrapper;
}
Android 6.0 Bitmap.cpp
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp#doFreePixels
Bitmap::Bitmap(JNIEnv* env, jbyteArray storageObj, void* address,
const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
: mPixelStorageType(PixelStorageType::Java) {
env->GetJavaVM(&mPixelStorage.java.jvm);
// 像素数据指针(在回收过程源码中会用到)
// 由于 strongObj 是局部变量,不能跨线程和跨方法使用,所以这里升级为弱全局引用
mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj);
mPixelStorage.java.jstrongRef = nullptr;
mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable));
mPixelRef->unref();
}
与 Android 8.0 对比区别不大,关键区别是像素数据内存的方式不一样:
至此,Native Bitmap 和像素数据内存空间都准备好了,SkBitmap 也成功获得了指向像素数据的指针。
上一节我们分析了 Bitmap 的创建过程,有创建就会有释放,这一节我们来分析 Bitmap 的内收过程,我们继续从 Android 6.0 和 Android 8.0 展开分析:
3.1 recycle() 回收方法
Java Bitmap 对象提供了 recycle() 方法主动释放内存资源,内部会调用 native 方法来释放 Native 内存。调用 recycle() 后的 Bitmap 对象会被标记为 “死亡” 状态,内部大部分方法都不在允许使用。因为不管像素数据是存在 Java 堆还是 Native 堆,Native Bitmap 这部分内存永远是在 Native 内存的,所以 native 方法这一步少不了。
Bitmap.java
// 回收标记位
private boolean mRecycled;
public void recycle() {
if (!mRecycled) {
// 括号内这部分在不同版本略有区别,但差别不大
// 调用 native 方法释放内存
nativeRecycle(mNativePtr);
mRecycled = true;
}
}
public final boolean isRecycled() {
return mRecycled;
}
public final int getWidth() {
if (mRecycled) {
Log.w(TAG, "Called getWidth() on a recycle()'d bitmap! This is undefined behavior!");
}
return mWidth;
}
3.2 Android 8.0 回收过程分析
同理,我们先分析 Android 8.0 的回收过程。
主动调用 recycle() 源码分析:Java 层调用的 recycle() 方法最终会走到 Native 层 Bitmap_recycle(…) 函数中,源码摘要如下:
Android 8.0 Bitmap.java
public void recycle() {
if (!mRecycled) {
nativeRecycle(mNativePtr);
mNinePatchChunk = null;
mRecycled = true;
}
}
// 使用 Native Bitmap 指针来回收
private static native void nativeRecycle(long nativeBitmap);
关联的 JNI 函数:
Android 8.0 graphics/Bitmap.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
// Java native 方法关联的 JNI 函数
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
// 根据分配过程的分析,我们知道 bitmapHandle 是 BitmapWrapper 类型
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}
class BitmapWrapper {
public:
BitmapWrapper(Bitmap* bitmap): mBitmap(bitmap) { }
void freePixels() {
...
mBitmap.reset();
}
...
private:
// Native Bitmap 指针
sk_sp<Bitmap> mBitmap;
...
};
不过,你会发现 hwui/Bitmap.cpp 中并没有 reset() 方法,那 reset() 到底是哪里来的呢?只能从 sk_sp<> 入手了,其实前面的源码中也出现过 sk_sp 泛型类,现在找一下它的定义:
Android 8.0 SkRefCnt.h
http://androidxref.com/8.0.0_r4/xref/external/skia/include/core/SkRefCnt.h#fPtr
// 共享指针泛型类,内部维持一个引用计数,并在指针引用计数归零时调用泛型实参的析构函数
template <typename T> class sk_sp {
public:
void reset(T* ptr = nullptr) {
T* oldPtr = fPtr;
fPtr = ptr;
oldPtr.unref();
}
private:
T* fPtr;
};
原来 sk_sp<> 是 Skia 内部定义的一个泛型类,能够实现共享指针在引用计数归零时自动调用对象的析构函数。这说明 reset() 最终会走到 hwui/Bitmap.cpp 的析构函数,并在 PixelStorageType::Heap 分支中通过 free() 释放先前 calloc() 动态分配的内存。Nice,闭环了。不仅 Native Bitmap 会析构,并且像素数据内存也会释放。
Android 8.0 hwui/Bitmap.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp
Bitmap::~Bitmap() {
switch (mPixelStorageType) {
case PixelStorageType::External:
// 外部方式(在源码中未查到找相关调用)
mPixelStorage.external.freeFunc(mPixelStorage.external.address, mPixelStorage.external.context);
break;
case PixelStorageType::Ashmem:
// mmap ashmem 内存(用于跨进程传递 Bitmap,例如 Notification)
munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
close(mPixelStorage.ashmem.fd);
break;
case PixelStorageType::Heap:
// Native 堆内存
// mPixelStorage.heap.address 在上文提到了
free(mPixelStorage.heap.address);
break;
case PixelStorageType::Hardware:
// 硬件位图
auto buffer = mPixelStorage.hardware.buffer;
buffer->decStrong(buffer);
mPixelStorage.hardware.buffer = nullptr;
break;
}
android::uirenderer::renderthread::RenderProxy::onBitmapDestroyed(getStableID());
}
引用机制兜底源码分析:在 Bitmap 构造器中,会创建 NativeAllocationRegistry 工具类来辅助回收 Native 内存,它背后利用了引用类型感知垃圾回收时机的机制,从而实现 Java Bitmap 对象被垃圾回收时确保回收底层 Native 内存。源码摘要如下:
Android 8.0 Bitmap.java
// 从 JNI 层调用
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
// NativeBitmap 指针
mNativePtr = nativeBitmap;
// 创建 NativeAllocationRegistry 工具
// 1. nativeGetNativeFinalizer(): Native 层回收函数指针
// 2. nativeSize:Native 内存占用大小
// 3. this:Java Bitmap
// 4. nativeBitmap:Native 对象指针
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
}
public final int getAllocationByteCount() {
return nativeGetAllocationByteCount(mNativePtr);
}
// 获取 Native 层回收函数的函数指针
private static native long nativeGetNativeFinalizer();
// 获取 Native 内存占用
private static native int nativeGetAllocationByteCount(long nativeBitmap);
Android 8.0 NativeAllocationRegistry.javahttp://androidxref.com/8.0.0_r4/xref/libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java
public class NativeAllocationRegistry {
private final ClassLoader classLoader;
private final long freeFunction;
private final long size;
public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
this.classLoader = classLoader;
this.freeFunction = freeFunction;
this.size = size;
}
public Runnable registerNativeAllocation(Object referent, long nativePtr) {
// 1. 向虚拟机声明 Native 内存占用
registerNativeAllocation(this.size);
// 2. 创建 Cleaner 工具类(本质上是封装了虚引用与引用队列)
Cleaner cleaner = Cleaner.create(referent, new CleanerThunk(nativePtr));
return new CleanerRunner(cleaner);
}
// 3. Cleaner 机制的回收函数
private class CleanerThunk implements Runnable {
private long nativePtr;
public CleanerThunk(long nativePtr) {
this.nativePtr = nativePtr;
}
public void run() {
// 4. 调用 Native 函数
applyFreeFunction(freeFunction, nativePtr);
// 5. 向虚拟机声明 Native 内存释放
registerNativeFree(size);
}
}
private static void registerNativeAllocation(long size) {
VMRuntime.getRuntime().registerNativeAllocation((int)Math.min(size, Integer.MAX_VALUE));
}
private static void registerNativeFree(long size) {
VMRuntime.getRuntime().registerNativeFree((int)Math.min(size, Integer.MAX_VALUE));
}
public static native void applyFreeFunction(long freeFunction, long nativePtr);
}
关联的 JNI 函数:
Android 8.0 libcore_util_NativeAllocationRegistry.cpp
http://androidxref.com/8.0.0_r4/xref/libcore/luni/src/main/native/libcore_util_NativeAllocationRegistry.cpp
// FreeFunction 是函数指针
typedef void (*FreeFunction)(void*);
static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*, jclass, jlong freeFunction, jlong ptr) {
// 执行函数指针指向的回收函数
void* nativePtr = reinterpret_cast<void*>(static_cast<uintptr_t>(ptr));
FreeFunction nativeFreeFunction = reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction));
nativeFreeFunction(nativePtr);
}
这个回收函数就是 Bitmap.java 中的 native 方法 nativeGetNativeFinalizer() 返回的函数指针:
graphics/Bitmap.cpp
http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp#mBitmap
// Java native 方法关联的 JNI 函数
static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
// 返回 Bitmap_destruct() 的地址
return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}
static void Bitmap_destruct(BitmapWrapper* bitmap) {
// 执行 delete 释放 Native Bitmap,最终会执行 Native Bitmap 的析构函数
delete bitmap;
}
可以看到,Bitmap 就是拿到一个 Native 层的回收函数然后注册到 NativeAllocationRegistry 工具里,NativeAllocationRegistry 内部再通过 Cleaner 机制包装了一个回收函数 CleanerThunk 。最终,当 Java Bitmap 被垃圾回收时,就会在 Native 层 delete Native Bitmap 对象,随即执行析构函数,也就衔接到最后 free 像素数据内存的地方。
示意图如下:
3.3 Android 6.0 回收过程分析
现在我们来分析 Android 6.0 上的 Bitmap 回收过程,相似的步骤我们不会过度分析。
主动调用 recycle() 源码分析:
Java 层调用的 recycle() 方法会走到 Native 层,关联的 JNI 函数:
Android 6.0 Bitmap.cpp
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp#doFreePixels
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
// 根据分配过程的分析,我们知道 bitmapHandle 是 Bitmap 类型
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}
void Bitmap::freePixels() {
doFreePixels();
mPixelStorageType = PixelStorageType::Invalid;
}
void Bitmap::doFreePixels() {
switch (mPixelStorageType) {
case PixelStorageType::Invalid:
// already free'd, nothing to do
break;
case PixelStorageType::External:
// 外部方式(在源码中未查到找相关调用)
mPixelStorage.external.freeFunc(mPixelStorage.external.address, mPixelStorage.external.context);
break;
case PixelStorageType::Ashmem:
// mmap ashmem 内存(用于跨进程传递 Bitmap,例如 Notification)
munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
close(mPixelStorage.ashmem.fd);
break;
case PixelStorageType::Java:
// Java 堆内存
// mPixelStorage.java.jweakRef 在上文提到了
JNIEnv* env = jniEnv();
// 释放弱全局引用
env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
break;
}
if (android::uirenderer::Caches::hasInstance()) {
android::uirenderer::Caches::getInstance().textureCache.releaseTexture( mPixelRef->getStableID());
}
}
可以看到,调用 recyele() 最终只是释放了像素数据数组的弱全局引用。
Finalizer 机制兜底源码分析:
在 Bitmap 的 finalize() 方法中,会调用 Native 方法辅助回收 Native 内存。源码摘要如下:
Android 6.0 Bitmap.java
http://androidxref.com/6.0.1_r10/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java#nativeRecycle
// 静态内部类 BitmapFinalizer:
public void finalize() {
setNativeAllocationByteCount(0);
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
关联的 JNI 函数:
static void Bitmap_destructor(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->detachFromJava();
}
void Bitmap::detachFromJava() {
...
// 释放当前对象
delete this;
}
// 析构函数也会调用 doFreePixels()
Bitmap::~Bitmap() {
doFreePixels();
}
可以看到,finalize() 最终会调用 delete 释放 Native Bitmap。如果没有主动调用 recycle(),在 Native Bitmap 的析构函数中也会走到 doFreePixels()。
示意图如下:
到这里,Bitmap 的分配和回收过程就分析完了。你会发现在 Android 8.0 以前的版本,Bitmap 的像素数据是存在 Java 堆的,Bitmap 数据放在 Java 堆容易造成 Java OOM,也没有完全利用起来系统 Native 内存。那么,有没有可能让低版本也将 Bitmap 数据存在 Native 层呢?关注我,带你建立核心竞争力,我们下次见。
参考资料
https://time.geekbang.org/column/article/71277
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!